/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package worker;

import data.BOMData;
import data.BOMHeader;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 *
 * @author bsmith
 */
public class BOMDataWorker {

    private static final BOMDataWorker bomDataWorkerIntance = new BOMDataWorker();
    private static Pattern pStart;
    private static Pattern pTime;
    private static Pattern pTimeAP;
    private static Pattern pTimeZone;
    private static Pattern pDayWeek;
    private static Pattern pDayMonth;
    private static Pattern pMonth;
    private static Pattern pYear;

    private BOMDataWorker() {
    }

    public static BOMDataWorker getInstance() {
        return bomDataWorkerIntance;
    }

    public List<BOMData> getDataFromJSONObjects(BOMHeader bomHeader, JSONObject jsonData) throws ParseException, Exception {
        List<BOMData> convertedData = null;
        try {
            convertedData = _getFromJSONObjects(bomHeader, jsonData);
        } catch (JSONException ex) {
            Logger.getLogger(BOMDataWorker.class.getName()).log(Level.SEVERE, null, ex);
        }
        return convertedData;
    }

    private List<BOMData> _getFromJSONObjects(BOMHeader bomHeader, JSONObject jsonData) throws JSONException, ParseException, Exception {
        List<BOMData> convertedData = new ArrayList<BOMData>();

        JSONObject observations = jsonData.getJSONObject("observations");
        JSONArray data = observations.getJSONArray("data");
        for (int i = 0; i < data.length(); i++) {
            BOMData bomdata = new BOMData();
            convertedData.add(bomdata);
            JSONObject dobj = data.getJSONObject(i);
            bomdata.setAifstime_utc(parseFloat(dobj.get("aifstime_utc")));
            bomdata.setAir_temp(parseFloat(dobj.get("air_temp")));
            bomdata.setApparent_t(parseFloat(dobj.get("apparent_t")));
            bomdata.setCloud((String) dobj.get("cloud"));
            bomdata.setCloud_base_m(parseFloat(dobj.get("cloud_base_m")));
            bomdata.setCloud_oktas(parseFloat(dobj.get("cloud_oktas")));
            bomdata.setCloud_type((String) dobj.get("cloud_type"));
            bomdata.setCloud_type_id(parseInteger(dobj.get("cloud_type_id")));
            bomdata.setDelta_t(parseFloat(dobj.get("delta_t")));
            bomdata.setDewpt(parseFloat(dobj.get("dewpt")));
            bomdata.setGust_kmh(parseFloat(dobj.get("gust_kmh")));
            bomdata.setGust_kt(parseFloat(dobj.get("gust_kt")));
            bomdata.setHistory_product((String) dobj.get("history_product"));
            bomdata.setLat(parseFloat(dobj.get("lat")));
            bomdata.setLocal_date_time(parseDate(bomHeader, dobj.get("local_date_time")));
            bomdata.setLocal_date_time_full(parseDate(bomHeader, dobj.get("local_date_time_full")));
            bomdata.setLon(parseFloat(dobj.get("lon")));
            bomdata.setName((String) dobj.get("name"));
            bomdata.setPress(parseFloat(dobj.get("press")));
            bomdata.setPress_msl(parseFloat(dobj.get("press_msl")));
            // bomdata.setPress_msl(bomdata.get);
            bomdata.setPress_qnh(parseFloat(dobj.get("press_qnh")));
            bomdata.setPress_tend((String) dobj.get("press_tend"));
            bomdata.setRain_trace(parseFloat(dobj.get("rain_trace")));
            bomdata.setRel_hum(parseFloat(dobj.get("rel_hum")));
            bomdata.setSea_state((String) dobj.get("sea_state"));
            bomdata.setSort_order(parseFloat(dobj.get("sort_order")));
            bomdata.setSwell_dir_worded((String) dobj.get("swell_dir_worded"));
            bomdata.setSwell_height(parseFloat(dobj.get("swell_height")));
            bomdata.setSwell_period(parseFloat(dobj.get("swell_period")));
            bomdata.setVis_km(parseFloat(dobj.get("vis_km")));
            bomdata.setWeather((String) dobj.get("weather"));
            bomdata.setWind_dir((String) dobj.get("wind_dir"));
            bomdata.setWind_spd_kmh(parseFloat(dobj.get("wind_spd_kmh")));
            bomdata.setWind_spd_kt(parseFloat(dobj.get("wind_spd_kt")));
            bomdata.setWmo(parseInteger(dobj.get("wmo")));
        }
        return convertedData;
    }

    private Float parseFloat(Object data) {
        if (data == null || JSONObject.NULL.equals(data)) {
            return new Float(0.0);
        }
        if (data instanceof Double) {
            return ((Double) data).floatValue();
        }
        if (data instanceof Integer) {
            return ((Integer) data).floatValue();
        }
        String sdata = (String) data;
        if (sdata.length() == 0) {
            return new Float(0.0);
        }
        Float result = null;
        try {
            result = Float.parseFloat(sdata);
        } catch (NumberFormatException nfe) {
            return new Float(0.0);
        }
        return result;
    }

    private Integer parseInteger(Object data) {
        if (data == null || JSONObject.NULL.equals(data)) {
            return new Integer(0);
        }
        if (data instanceof Integer) {
            return ((Integer) data).intValue();
        }

        String sdata = (String) data;
        if (sdata.length() == 0) {
            return new Integer(0);
        }
        return Integer.parseInt(sdata);
    }

    private Long parseLong(Object data) {
        if (data == null || JSONObject.NULL.equals(data)) {
            return new Long(0);
        }
        if (data instanceof Long) {
            return ((Long) data).longValue();
        }

        String sdata = (String) data;
        if (sdata.length() == 0) {
            return new Long(0);
        }
        return Long.parseLong(sdata);
    }

    private Date parseDate(BOMHeader bomHeader, Object data) throws ParseException, Exception {
        if (data == null || JSONObject.NULL.equals(data)) {
            return null;
        }
        if (data instanceof Date) {
            return ((Date) data);
        }

        if (((String) data).contains("/")) {
            return extractDateFromBOMAbreviatedShortDate(bomHeader.getRefreshDateFromMessage(), (String) data);//DateFormat.getInstance().parse((String) data);
        }
        Long sdata = Long.parseLong((String) data);

        return new Date(sdata);
    }

    /**
     * Return map from date on bomdata to bomdata object for all data read.
     *
     * @param bomDataPoints
     * @return
     * @throws Exception
     */
    public Map<Date, BOMData> listToDateMap(List<BOMData> bomDataPoints) throws Exception {
        Map<Date, BOMData> dateMap = new HashMap<Date, BOMData>();

        for (BOMData bomData : bomDataPoints) {
            if (bomData.getLocal_date_time() != null) {
                dateMap.put(bomData.getLocal_date_time(), bomData);
            } else {
                throw new Exception("Date missing from bomData:" + bomData);
            }
        }
        return dateMap;
    }

    /**
     *
     * @param bomDataMap - map from date of data tot he data itself
     * @param dateOfInterest - given date
     * @return List sorted by time for the BOMData on the given date
     */
    public List<BOMData> getSortedBomDataListOnGivenDay(Map<Date, BOMData> bomDataMap, Date dateOfInterest) {
        TreeSet<Date> sortedDates = new TreeSet(bomDataMap.keySet());
        List<BOMData> listSortedBOMDataForDate = new ArrayList<BOMData>();
        for (Date d : sortedDates) {
            if (dateOfInterest.getYear() == d.getYear() && dateOfInterest.getMonth() == d.getMonth() && dateOfInterest.getDay() == d.getDay()) {
                listSortedBOMDataForDate.add(bomDataMap.get(d));
            }
        }
        return listSortedBOMDataForDate;
    }

    public BOMHeader getHeaderFromJSONObjects(JSONObject json) throws ParseException, Exception {
        BOMHeader convertedHeader = null;
        try {
            convertedHeader = _getHeaderFromJSONObjects(json);
        } catch (JSONException ex) {
            Logger.getLogger(BOMDataWorker.class.getName()).log(Level.SEVERE, null, ex);
        }
        return convertedHeader;
    }

    private BOMHeader _getHeaderFromJSONObjects(JSONObject json) throws JSONException, ParseException, Exception {
        BOMHeader convertedHeader = new BOMHeader();

        JSONObject observations = json.getJSONObject("observations");
        assert (observations.getJSONArray("header").length() == 1);
        JSONObject header = observations.getJSONArray("header").getJSONObject(0);   // header only has one object
        convertedHeader.setRefresh_message((String) header.get("refresh_message"));
        convertedHeader.setRefreshDateFromMessage(extractRefreshDateFromMessage(convertedHeader.getRefresh_message()));
        convertedHeader.setID((String) header.get("ID"));
        convertedHeader.setMain_ID((String) header.get("main_ID"));
        convertedHeader.setName((String) header.get("name"));
        convertedHeader.setState_time_zone((String) header.get("state_time_zone"));
        convertedHeader.setTime_zone((String) header.get("time_zone"));
        convertedHeader.setProduct_name((String) header.get("product_name"));
        convertedHeader.setState((String) header.get("state"));

        return convertedHeader;
    }

    static {
        pStart = Pattern.compile("Issued at", Pattern.CASE_INSENSITIVE);
        pTime = Pattern.compile("(\\d{1,2}):(\\d{2})(am|pm)?[\\s]*", Pattern.CASE_INSENSITIVE);
        pTimeAP = Pattern.compile("(am|pm)", Pattern.CASE_INSENSITIVE);
        pTimeZone = Pattern.compile("EST", Pattern.CASE_INSENSITIVE);
        pDayWeek = Pattern.compile("(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)", Pattern.CASE_INSENSITIVE);
        pDayMonth = Pattern.compile("\\d{1,2}");
        pMonth = Pattern.compile("(January|February|March|April|May|June|July|August|September|October|November|December)", Pattern.CASE_INSENSITIVE);
        pYear = Pattern.compile("\\d{4}");
    }

    /**
     * @param refreshMessage eg. "Issued at 8:35 pm EST Friday 27 April 2012"
     * @return date extracted from this
     */
    protected Date extractRefreshDateFromMessage(String refreshMessage) throws Exception {
        Date refreshDate = null;
        List<Pattern> patternsUsed = new ArrayList();
        patternsUsed.add(pStart);
        patternsUsed.add(pTime);
        patternsUsed.add(pTimeAP);
        patternsUsed.add(pTimeZone);
        patternsUsed.add(pDayWeek);
        patternsUsed.add(pDayMonth);
        patternsUsed.add(pMonth);
        patternsUsed.add(pYear);
        Integer dayOfMonth = null;
        String month = null;
        Integer year = null;

        String[] refreshMessageParts = refreshMessage.split("[\\s]+");
        for (String part : refreshMessageParts) {
            for (Pattern p : patternsUsed) {
                Matcher m = p.matcher(part);
                if (m.matches()) {
                    if (p.equals(pDayMonth)) {
                        dayOfMonth = Integer.parseInt(m.group());
                    }
                    if (p.equals(pMonth)) {
                        month = m.group();
                    }
                    if (p.equals(pYear)) {
                        year = Integer.parseInt(m.group());
                    }
                }
            }
        }
        if (dayOfMonth == null || month == null || year == null) {
            throw new Exception("Unable to extract day month year from \"" + refreshMessage + "\"");
        }

        String str = String.format("%d %s %d", dayOfMonth, month, year);
        DateFormat formatter = new SimpleDateFormat("dd MMM yyyy");
        refreshDate = (Date) formatter.parse(str);
        return refreshDate;
    }

    /**
     * @param bomShortDate eg. "29/09:00am"
     * @return Date from data and year, month of Header's refreshMessage.
     */
    protected Date extractDateFromBOMAbreviatedShortDate(Date headerRefreshDate, String bomShortDate) throws Exception {
        List<Pattern> patternsUsed = new ArrayList();
        patternsUsed.add(pDayMonth);
        patternsUsed.add(pTime);
        Integer dayOfMonth = null;
        Integer hours = null;
        Integer mins = null;
        String ampm = null;

        String[] bomShortDateParts = bomShortDate.split("/");
        for (String part : bomShortDateParts) {
            for (Pattern p : patternsUsed) {
                Matcher m = p.matcher(part);
                if (m.matches()) {
                    if (p.equals(pDayMonth)) {
                        dayOfMonth = Integer.parseInt(m.group());
                    }
                    if (p.equals(pTime)) {
                        hours = Integer.parseInt(m.group(1));
                        mins = Integer.parseInt(m.group(2));
                        ampm = m.group(3);
                    }
                }
            }
        }
        if (dayOfMonth == null || hours == null || mins == null) {
            throw new Exception("Unable to extract day, hour and min \"" + bomShortDate + "\"");
        }
        Calendar cHeaderRefreshDate = new GregorianCalendar();
        cHeaderRefreshDate.setTime(headerRefreshDate);
        // Set the hours and mins to same as other Calendar so comparison works
        cHeaderRefreshDate.set(Calendar.HOUR, hours);
        cHeaderRefreshDate.set(Calendar.MINUTE, mins);
        if (ampm.equalsIgnoreCase("AM")) {
            cHeaderRefreshDate.set(Calendar.AM_PM, Calendar.AM);
        } else {
            cHeaderRefreshDate.set(Calendar.AM_PM, Calendar.PM);
        }

        Calendar c = new GregorianCalendar(cHeaderRefreshDate.get(Calendar.YEAR), cHeaderRefreshDate.get(Calendar.MONTH), dayOfMonth, hours, mins);
        if (ampm.equalsIgnoreCase("AM")) {
            c.set(Calendar.AM_PM, Calendar.AM);
        } else {
            c.set(Calendar.AM_PM, Calendar.PM);
        }
        //System.out.println("  (Result before subtract) - " + printCal(c));
        if (c.after(cHeaderRefreshDate)) {
            c.add(Calendar.MONTH, -1);
        }
        //System.out.println("-> extractDateFromBOMAbreviatedShortDate(headerRefreshDate:" + headerRefreshDate + ", \n\tbomShortDate:" + bomShortDate + ", \n\tdayOfMonth:" + dayOfMonth + ", \n\tcHeaderRefreshDate.get(Calendar.DAY_OF_MONTH):" + cHeaderRefreshDate.get(Calendar.DAY_OF_MONTH) + ", \n\tresult:" + printCal(c));
        //System.out.println("-> extractDateFromBOMAbreviatedShortDate(headerRefreshDate - date:" + printCal(c));
        return c.getTime();
    }

    private String printCal(Calendar c) {
        StringBuffer sb = new StringBuffer();
        sb.append("Year:").append(c.get(Calendar.YEAR));
        sb.append(", Month:").append(c.get(Calendar.MONTH));
        sb.append(", Day:").append(c.get(Calendar.DAY_OF_MONTH));
        sb.append(", Hour:").append(c.get(Calendar.HOUR));
        sb.append(", Min:").append(c.get(Calendar.MINUTE));
        sb.append(", AMPM:").append(c.get(Calendar.AM_PM));
        return sb.toString();
    }

    public int getMinimumTempIndex(List<BOMData> bomDataListForDate) {
        float min = 1000;
        int index = 0;
        int savedIndex = 0;
        for (BOMData bd : bomDataListForDate) {
            if (bd.getAir_temp() < min) {
                min = bd.getAir_temp();
                savedIndex = index;
            }
            index++;
        }
        return savedIndex;
    }

    public int getMaximumTempIndex(List<BOMData> bomDataListForDate) {
        float max = -1000;
        int index = 0;
        int savedIndex = 0;
        for (BOMData bd : bomDataListForDate) {
            if (bd.getAir_temp() > max) {
                max = bd.getAir_temp();
                savedIndex = index;
            }
            index++;
        }
        return savedIndex;
    }

    public int getLatestTempIndex(List<BOMData> bomDataListForDate) {
        return bomDataListForDate.size() - 1;
    }

    /**
     * Calculate the rainfall from 9am one day to 9am- on the next
     *
     * @param bomDataListForYesterday - the time sorted BOMData for 'yesterday'
     * @param bomDataListForToday - the time sorted BOMData for 'today'
     * @return the rainfall from 9am yesterday to just before 9am today.
     */
    public float getRainFallBefore9(List<BOMData> bomDataListForYesterday, List<BOMData> bomDataListForToday) {
        // grab the entry that proceeded 9am today
//        Calendar yesterday9am = getDate9am(bomDataListForYesterday.get(0).getLocal_date_time());
        if (bomDataListForToday == null || bomDataListForToday.size() == 0) {
            Logger.getLogger(BOMDataWorker.class.getName()).log(Level.WARNING, "bomDataListForToday == null || bomDataListForToday.size() == 0");
            return 0;
        }
        Calendar today9am = getDate9am(bomDataListForToday.get(0).getLocal_date_time());
        float rainfall = 0.0f;

        // Reverse the list (so from most recent time) and grab the entry that follows 9am
        Collections.reverse(bomDataListForToday);
        for (BOMData b : bomDataListForToday) {
            Calendar dataDate = Calendar.getInstance();
            dataDate.setTime(b.getLocal_date_time());
            if (dataDate.before(today9am)) {
                rainfall = b.getRain_trace();
                break;
            }
        }
        return rainfall;
    }

    /**
     * @param local_date_time is a date with whatever hour:min ...,
     * @return same date at 9am precisely.
     */
    private Calendar getDate9am(Date local_date_time) {
        Calendar today9amC = Calendar.getInstance();
        today9amC.setTime(local_date_time);
        today9amC.set(Calendar.HOUR, 9);
        today9amC.set(Calendar.MINUTE, 0);
        today9amC.set(Calendar.SECOND, 0);
        today9amC.set(Calendar.MILLISECOND, 0);
        today9amC.set(Calendar.AM_PM, Calendar.AM);
        return today9amC;
    }

    /**
     *
     * @param bomDataListForToday
     * @param date
     * @return the rainfall that has fallen from 9am on this day
     */
    public float getRainFallAfter9(List<BOMData> bomDataListForToday, Date date) {
        // just grab the last one
        if (bomDataListForToday == null || bomDataListForToday.size() < 1) {
            Logger.getLogger(BOMDataWorker.class.getName()).log(Level.WARNING, "bomDataListForToday == null || bomDataListForToday.size() == 0");
            return 0;
        }

        float rainfall = bomDataListForToday.get(bomDataListForToday.size() - 1).getRain_trace();
        return rainfall;
    }

    public String getObservationString(List<BOMData> bomDataListForToday) {
        StringBuffer sb = new StringBuffer();
        for (BOMData b : bomDataListForToday) {
            if ((b.getLocal_date_time().getHours() == 9 || b.getLocal_date_time().getHours() == 6 || b.getLocal_date_time().getHours() == 12 || b.getLocal_date_time().getHours() == 0) && b.getLocal_date_time().getMinutes() == 0) {
                sb.append(getTimeString(b.getLocal_date_time())).append(":");
            }
            sb.append(b.getWeather()).append(", ");
        }
        return sb.toString();
    }

    public static String getTimeString(Date time) {
        return new SimpleDateFormat("H:mm").format(time);//d-m-y 
    }
}